﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Net;
using System.IO;
using System.Threading.Tasks;
using AppConfigSettings;
using ButsaLib;
using ButsaLib.Models;
using CustomConfigurationSettings;
using HtmlAgilityPack;
using System.Text.RegularExpressions;

namespace ButsaScraperLib
{
	public sealed class ButsaManager
	{
		//TODO: уйти от singletone и передавать конфигурацию в конструктор или через injection
		private readonly IConfigurationManager _configurationManager;

		private volatile static ButsaManager _instance;

		private static readonly object SyncRoot = new object();

		public static ButsaManager Instance
		{
			get
			{
				if (_instance == null)
				{
					lock (SyncRoot)
					{
						if (_instance == null)
							_instance = new ButsaManager();
					}
				}
				return _instance;
			}
		}

		private ButsaManager()
		{
			_configurationManager = new AppConfigManager();
		}

		private readonly char[] _trimSymbols = new[] { ' ', '_', '\n' };

		public string LoadPage(string url, bool anonymously = false)
		{
			HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create(url);
			httpWebRequest.Proxy = WebRequest.DefaultWebProxy;
			httpWebRequest.ServicePoint.Expect100Continue = false;
			httpWebRequest.Method = "POST";
			httpWebRequest.ContentType = "application/x-www-form-urlencoded";
			Stream requestStream = httpWebRequest.GetRequestStream();
			if (!anonymously)
			{
				byte[] bytes = Encoding.GetEncoding(1251).GetBytes(String.Format("auth_name={0}&auth_pass={1}",
				                                                                 _configurationManager.GetConfigurationReader.
				                                                                 	GetConfigurationValue("butsalogin"),
				                                                                 _configurationManager.GetConfigurationReader.
				                                                                 	GetConfigurationValue("butsapassword")));
				requestStream.Write(bytes, 0, bytes.Length);
				requestStream.Close();
			}
			WebResponse webResponse = httpWebRequest.GetResponse();
			string loadedPage = new StreamReader(webResponse.GetResponseStream(), Encoding.GetEncoding("UTF-8")).ReadToEnd();
			return loadedPage;
		}

		/// <summary>
		/// Чтение всех команд для создания кэша TeamCache.
		/// </summary>
		/// <returns></returns>
		public void ReadAllTeamsToCache(EventHandler callback)
		{
			//начальная страница
			//http://butsa.ru/xml/ratings/ratings.php?page=1&type=ratings/teams&act=select&id=28&class=1
			//примерно 228 страниц (11360), на каждой странице 50 команд

			const string templateUrl = "http://butsa.ru/xml/ratings/ratings.php?page={0}&type=ratings/teams&act=select&id=28&class=1";
			const string tablePath = "/body[1]/div[1]/table[1]/tr[3]/td[1]/table[1]/tr[1]/td[2]/table[1]/tr[1]/td[2]/table[1]/tr[7]/td[1]" +
			                         "/table[1]/tr[1]/td[2]/table[1]/tr[2]/td[1]/table[1]/tr[1]/td[1]/table[2]";
			const string cellParserPattern = ".*title=\"(?<teamCountry>[^\"]+)\".*/roster/(?<teamId>\\d+)\".*\\>(?<teamName>[^<]+)</a>.*";
			var cellParserRx = new Regex(cellParserPattern);


			List<TeamCache> teamCaches = new List<TeamCache>();
			bool fetchNext = true;
			int pageNumber = 1;
			while (fetchNext)
			{
				string url = String.Format(templateUrl, pageNumber);
				string pageHtml = LoadPage(url);

				//разбор страницы
				HtmlDocument htmlDocument = new HtmlDocument();
				htmlDocument.Load(new StringReader(pageHtml));
				var table = htmlDocument.DocumentNode.SelectSingleNode(tablePath);

				//информация по командам начинается со второй строки
				var rowsCount = table.ChildNodes.Count(item => String.Compare(item.Name, "tr", true, CultureInfo.InvariantCulture) == 0);

				for (int i = 2; i < rowsCount; i++)
				{
					var teamCell = table.SelectSingleNode(String.Format("tr[{0}]/td[2]", i));
					var countryName = table.SelectSingleNode(String.Format("tr[{0}]/td[3]", i)).InnerText.Trim(_trimSymbols);
					var matchResult = cellParserRx.Match(teamCell.InnerHtml);
					if (matchResult.Success)
					{
						teamCaches.Add(new TeamCache
						               	{
						               		TeamName = matchResult.Groups["teamName"].Value,
						               		TeamId = matchResult.Groups["teamId"].Value,
											CountryName = countryName
						               	});
					}
					else
					{
						Debug.WriteLine(teamCell.InnerHtml);
					}
				}
				
				fetchNext = rowsCount == 52; //одна лишняя строка в шапку, другая в футер
				
				if (callback != null)
					if (fetchNext)
						callback(pageNumber, new EventArgs());
					else
						callback(null, new EventArgs());

				pageNumber++;
			}
			ButsaDbContext dbContext = new ButsaDbContext();
			dbContext.Database.ExecuteSqlCommand("truncate table dbo.TeamCache");
			for (int i = 0; i < teamCaches.Count; i++)
				dbContext.TeamsCache.Add(teamCaches[i]);
			dbContext.SaveChanges();
		}

		/// <summary>
		/// Заполнения кэша всех дивизионов.
		/// </summary>
		/// <param name="callback"></param>
		public void ReadAllDivisionsToCache(EventHandler callback)
		{
			//начальная страница
			const string url = "http://butsa.ru/ratings/";

			const string listPath =
				"/body[1]/div[1]/table[1]/tr[3]/td[1]/table[1]/tr[1]/td[2]/table[1]/tr[1]/td[2]/table[1]/" +
				"tr[7]/td[1]/table[1]/tr[1]/td[2]/table[1]/tr[2]/td[1]/table[1]/tr[1]/td[1]/table[1]/" +
				"tr[2]/td[2]/select[1]";

			const string divisionPattern = "<option value=\"(?<divisionId>\\d+)\"\\s?>(?<divisionName>[^<]+)";
			const string countryNamePattern = "(?<countryName>[\\w-]+)-\\d(-\\w)?";
			var pairRegex = new Regex(divisionPattern, RegexOptions.IgnoreCase | RegexOptions.Multiline);
			var countryNameExtractRegex = new Regex(countryNamePattern, RegexOptions.Compiled);

			string pageHtml = LoadPage(url);

			//разбор страницы
			HtmlDocument htmlDocument = new HtmlDocument();
			htmlDocument.Load(new StringReader(pageHtml));
			var list = htmlDocument.DocumentNode.SelectSingleNode(listPath);
			
			List<DivisionCache> divisionCaches = new List<DivisionCache>();
			int i = 1;
			var match = pairRegex.Match(list.InnerHtml);
			while (match.Success)
			{
				string divisionName = match.Groups["divisionName"].Value.Trim(_trimSymbols);
				var countryMatch = countryNameExtractRegex.Match(divisionName);
				divisionCaches.Add(new DivisionCache
				                   	{
				                   		DivisionId = match.Groups["divisionId"].Value,
										DivisionName = divisionName,
										CountryName = countryMatch.Success ? countryMatch.Groups["countryName"].Value : String.Empty
				                   	});
				match = match.NextMatch();

				if (callback != null)
					if (match.Success)
						callback(i, new EventArgs());
					else
						callback(null, new EventArgs());
				i++;
			}

			ButsaDbContext dbContext = new ButsaDbContext();
			dbContext.Database.ExecuteSqlCommand("truncate table dbo.DivisionCache");
			for (int j = 0; j < divisionCaches.Count; j++)
				dbContext.DivisionsCache.Add(divisionCaches[j]);
			dbContext.SaveChanges();
		}

		/// <summary>
		/// Чтение снимков команд.
		/// </summary>
		/// <param name="teamIds">Идентификаторы команд, которые нужно снять.</param>
		/// <param name="callback"></param>
		/// <returns></returns>
		public List<TeamShot> ReadTeamsShots(List<string> teamIds, EventHandler callback)
		{
			//работают ссылки вида http://butsa.ru/roster/1505/, где 1505 - id команды
			//http://butsa.ru/xml/players/roster.php?id=1505&act=abilities - по умениям каждого игрока
			//id команд берутся из кэша
			const string teamUrlTemaplate = "http://butsa.ru/roster/";
			const string playersUrlTemaplate = "http://butsa.ru/xml/players/roster.php?id={0}&act=abilities";

			//roster: первый блок информации о команде
			const string teamInfo1Path = "/body[1]/div[1]/table[1]/tr[2]/td[1]/table[1]/tr[1]/td[2]/table[1]/tr[1]/td[2]/" +
			                             "table[1]/tr[3]/td[1]/table[1]/tr[1]/td[1]/td[1]/table[1]/tr[2]/td[1]/table[1]/" +
			                             "tr[1]/td[1]/table[1]/td[1]/table[1]";

			//roster: второй блок информации о команде
			const string teamInfo2Path = "/body[1]/div[1]/table[1]/tr[2]/td[1]/table[1]/tr[1]/td[2]/table[1]/tr[1]/td[2]/" +
										 "table[1]/tr[3]/td[1]/table[1]/tr[1]/td[1]/td[1]/table[1]/tr[2]/td[1]/table[1]/" +
										 "tr[1]/td[1]/table[1]/td[2]/table[1]";
			//roster: таблица с игроками
			const string playersPath = "/body[1]/div[1]/table[1]/tr[2]/td[1]/table[1]/tr[1]/td[2]/table[1]/tr[1]/td[2]/" +
			                           "table[1]/tr[3]/td[1]/table[1]/tr[1]/td[1]/td[1]/table[1]/tr[2]/td[1]/table[1]/" +
			                           "tr[1]/td[1]/table[2]/tr[1]/td[1]/table[1]"; 
			//abilities: таблица игроков
			const string playersAbibilitiesPath = "/body[1]/div[1]/table[1]/tr[2]/td[1]/table[1]/tr[1]/td[2]/table[1]/tr[1]/td[2]/" +
									   "table[1]/tr[3]/td[1]/table[1]/tr[1]/td[1]/td[1]/table[1]/tr[2]/td[1]/table[1]/" +
									   "tr[1]/td[1]/table[2]/tr[1]/td[1]/table[1]";

			//List<Team> teams = new List<Team>(teamIds.Count);
			List<TeamShot> teams = new List<TeamShot>(teamIds.Count);

			Regex playerIdExtractor = new Regex(@"/players/(?<id>\d*)", RegexOptions.Compiled);
			Regex playerMoralityExtractor = new Regex(@"(?<mrl>\d\d?)", RegexOptions.Compiled);

			Parallel.For(0, teamIds.Count, new ParallelOptions { MaxDegreeOfParallelism = 12 },
			//Parallel.For(0, 1, new ParallelOptions { MaxDegreeOfParallelism = 1 },
				(i, loopState) =>
					{
						string teamId = teamIds[i];
						//string teamId = "135";

						try
						{
							//загрузка страниц в документы
							string teamPageHtml = LoadPage(teamUrlTemaplate + teamId, true);
							string playersPageHtml = LoadPage(String.Format(playersUrlTemaplate, teamId), true);
							HtmlDocument teamPageDocument = new HtmlDocument();
							HtmlDocument playersPageDocument = new HtmlDocument();
							teamPageDocument.Load(new StringReader(teamPageHtml));
							playersPageDocument.Load(new StringReader(playersPageHtml));

							var teamInfo1 = teamPageDocument.DocumentNode.SelectSingleNode(teamInfo1Path);
							var teamInfo2 = teamPageDocument.DocumentNode.SelectSingleNode(teamInfo2Path);
							var playersTable = teamPageDocument.DocumentNode.SelectSingleNode(playersPath);
							var abilitiesTable = playersPageDocument.DocumentNode.SelectSingleNode(playersAbibilitiesPath);

							TeamShot team = new TeamShot();
							team.PlayerShots = new List<PlayerShot>();

							//чтение команды
							team.TeamId = Int32.Parse(teamId);
							team.TeamName = teamInfo1.SelectSingleNode("tr[1]/td[3]/span[1]/a[1]").InnerText;
							team.DivisionName = teamInfo2.SelectSingleNode("tr[1]/td[3]/span[1]/a[1]").InnerText;
							team.CountryName = team.DivisionName.Substring(0, team.DivisionName.LastIndexOf('-')); //TODO: неправильно определяется
							//чек на деньги
							bool moneyCheck = teamInfo2.SelectSingleNode("tr[2]/td[1]").InnerText.Contains("Деньги");

							int k = 4;
							if (!moneyCheck)
								k = 3;
							else
								team.Money = Double.Parse(String.Join(String.Empty, teamInfo2.SelectSingleNode("tr[2]/td[3]").InnerText
									.Trim(_trimSymbols)
									.Split(new char[] {'.'})), 
									CultureInfo.InvariantCulture);

							string forceString = teamInfo2.SelectSingleNode("tr[" + k + "]/td[3]/span[1]").InnerText.Trim(_trimSymbols);
							forceString = forceString.Substring(0, forceString.IndexOf(' '));
							team.Force = Double.Parse(forceString, CultureInfo.InvariantCulture);
							string top11ForceString = teamInfo2.SelectSingleNode("tr[" + (k + 1) + "]/td[3]/span[1]").InnerText.Trim(_trimSymbols);
							top11ForceString = top11ForceString.Substring(0, top11ForceString.IndexOf(' '));
							team.Top11Force = Double.Parse(top11ForceString, CultureInfo.InvariantCulture);
							team.Rating = Double.Parse(teamInfo2.SelectSingleNode("tr[" + (k + 2) + "]/td[3]/span[1]").InnerText.Trim(_trimSymbols), 
								CultureInfo.InvariantCulture);
							team.HasManager = !String.IsNullOrEmpty(teamInfo1.SelectSingleNode("tr[3]/td[3]/span[1]/nobr[1]/a[1]").InnerText.Trim(_trimSymbols));

							if (team.HasManager)
								team.Manager = teamInfo1.SelectSingleNode("tr[3]/td[3]/span[1]/nobr[1]/a[1]").InnerText.Trim(_trimSymbols);

							teams.Add(team);

							//чтение информации по каждому игроку
							//1-я и последняя строки игнорируются

							var playersTableRows = playersTable.SelectNodes("tr");
							var abilitiesTableRows = abilitiesTable.SelectNodes("tr");
							for (int j = 1; j < playersTableRows.Count - 1; j++)
							{
								var playerRow = playersTableRows[j];
								PlayerShot player = new PlayerShot();

								player.PlayerNumber = Int32.Parse(playerRow.SelectSingleNode("td[1]").InnerText.Trim(_trimSymbols));

								string playerIdHref = playerRow.SelectSingleNode("td[2]/a[1]").Attributes["href"].Value;
								Match playerIdMatch = playerIdExtractor.Match(playerIdHref);
								if (playerIdMatch.Success)
								{
									player.PlayerId = Int32.Parse(playerIdMatch.Groups["id"].Value);
								}
								else
								{
									Debug.WriteLine("player id extract fail\t" + player.PlayerId);
									throw new ApplicationException();
								}

								player.PlayerName = playerRow.SelectSingleNode("td[2]/a[1]").InnerText;
								player.IsDiscvalificated = playerRow.SelectSingleNode("td[2]").InnerHtml.Contains("дисквалификация");
								player.IsTraumed = playerRow.SelectSingleNode("td[2]").InnerHtml.Contains("травма");

								string position = playerRow.SelectSingleNode("td[4]").InnerText.Trim(_trimSymbols);
								Position primaryPosition;
								Position? secondaryPosition;
								CalculatePositions(position, out primaryPosition, out secondaryPosition);
								player.PrimaryPosition = primaryPosition.ToString();
								player.SecondaryPosition = secondaryPosition.HasValue? secondaryPosition.Value.ToString(): null as string;

								player.Age = Int32.Parse(playerRow.SelectSingleNode("td[5]").InnerText.Trim(_trimSymbols));
								player.Talent = Double.Parse(playerRow.SelectSingleNode("td[7]").InnerText.Trim(_trimSymbols), CultureInfo.InvariantCulture);
								player.PhysicalFitness = Int32.Parse(playerRow.SelectSingleNode("td[8]").InnerText.Trim(_trimSymbols));
								player.Tiredness = Double.Parse(playerRow.SelectSingleNode("td[9]").InnerText.Trim(_trimSymbols), CultureInfo.InvariantCulture);

								string moralityString = playerRow.SelectSingleNode("td[11]").Attributes["title"].Value;
								Match playerMoralityMatch = playerMoralityExtractor.Match(moralityString);
								if (playerMoralityMatch.Success)
								{
									player.Morality = Int32.Parse(playerMoralityMatch.Groups["mrl"].Value);
								}
								else
								{
									Debug.WriteLine("player morality extract fail\t" + moralityString);
									throw new ApplicationException();
								}

								string bonusString = playerRow.SelectSingleNode("td[12]").InnerText.Trim(_trimSymbols);
								var playerBonuses = ParsePlayerBonusesString(bonusString);
								player.BonusShots = new List<BonusShot>();
								player.BonusShots = playerBonuses
									.Select(item => new BonusShot { BonusName = item.Key.ToString(), BonusValue = item.Value })
									.ToList();

								//Умения
								var abilitiesRow = abilitiesTableRows[j];
								player.CurrentTotalSkill = Double.Parse(abilitiesRow.SelectSingleNode("td[6]").InnerText,
										CultureInfo.InvariantCulture);
								if (player.PrimaryPosition != "Gk")
								{
									player.TakingSkill = Double.Parse(abilitiesRow.SelectSingleNode("td[7]").InnerText,
										CultureInfo.InvariantCulture);
									player.MarkingSkill = Double.Parse(abilitiesRow.SelectSingleNode("td[8]").InnerText,
										CultureInfo.InvariantCulture);
									player.DribblingSkill = Double.Parse(abilitiesRow.SelectSingleNode("td[9]").InnerText,
										CultureInfo.InvariantCulture);
									player.ReceptionSkill = Double.Parse(abilitiesRow.SelectSingleNode("td[10]").InnerText,
										CultureInfo.InvariantCulture);
									player.StaminaSkill = Double.Parse(abilitiesRow.SelectSingleNode("td[11]").InnerText,
										CultureInfo.InvariantCulture);
									player.PassSkill = Double.Parse(abilitiesRow.SelectSingleNode("td[12]").InnerText,
										CultureInfo.InvariantCulture);
									player.StrikeValueSkill = Double.Parse(abilitiesRow.SelectSingleNode("td[13]").InnerText,
										CultureInfo.InvariantCulture);
									player.StrikeAccuracySkill = Double.Parse(abilitiesRow.SelectSingleNode("td[14]").InnerText,
										CultureInfo.InvariantCulture);
								}
								team.PlayerShots.Add(player);
							}
							callback(i, new EventArgs());
						}
						catch (Exception)
						{
							Debug.WriteLine(teamId);
						}
						Debug.WriteLine("teamNN\t" + DateTime.Now + "\t" + teams.Count);
					});
			return teams;
		}

		public List<SimpleResult> ReadResults(int tourNumber, List<int> divisionIds)
		{
			const string resultsUrlTemplate =
				"http://butsa.ru/xml/tour/index.php?act=fixtures&tour={0}&tournament=390&division={1}";
			const string resultsTablePath =
				"/body[1]/div[1]/table[1]/tr[3]/td[1]/table[1]/tr[1]/td[2]/table[1]/tr[1]/td[2]/table[1]/" +
				"tr[7]/td[1]/table[1]/tr[1]/td[2]/table[1]/tr[2]/td[1]/table[1]/tr[1]/td[1]/center[1]/table[1]";

			Regex teamCodeExtractorRegex = new Regex("<a href=\"/roster/(?<teamCode>.+)/\">.+</a>");
			Regex extractScore = new Regex(@"(?<hs>\d+):(?<gs>\d+)");

			List<SimpleResult> results = new List<SimpleResult>();

			Parallel.For(0, divisionIds.Count, new ParallelOptions {MaxDegreeOfParallelism = 12},
			//Parallel.For(0, divisionIds.Count, new ParallelOptions { MaxDegreeOfParallelism = 1 },
			             (i, loopState) =>
			             	{
								
			             		string pageUrl = String.Format(resultsUrlTemplate, tourNumber, divisionIds[i]);
			             		string resultsPageHtml = LoadPage(pageUrl);
			             		HtmlDocument resultsPageDocument = new HtmlDocument();
			             		resultsPageDocument.Load(new StringReader(resultsPageHtml));

			             		var resultsTable = resultsPageDocument.DocumentNode.SelectSingleNode(resultsTablePath);
			             		var resultsRows = resultsTable.SelectNodes("tr");
			             		for (int j = 1; j < resultsRows.Count; j++)
			             		{
									SimpleResult simpleResult = new SimpleResult();
			             			var row = resultsRows[j];
			             			var homeCommandHtml = row.SelectSingleNode("td[1]").InnerHtml;
									var scoreText = row.SelectSingleNode("td[2]/nobr[1]/a[1]").InnerText.Trim(_trimSymbols);
									var guestCommandHtml = row.SelectSingleNode("td[3]").InnerHtml;

			             			var homeTeamMatch = teamCodeExtractorRegex.Match(homeCommandHtml);
									var guestTeamMatch = teamCodeExtractorRegex.Match(guestCommandHtml);
									var scoreMatch = extractScore.Match(scoreText);
									if (homeTeamMatch.Success && guestTeamMatch.Success && scoreMatch.Success)
									{
										simpleResult.HomeTeamId = GetTeamIdByCode(homeTeamMatch.Groups["teamCode"].Value);
										simpleResult.GuestTeamId = GetTeamIdByCode(guestTeamMatch.Groups["teamCode"].Value);
										simpleResult.HomeTeamGoals = Int32.Parse(scoreMatch.Groups["hs"].Value);
										simpleResult.GuestTeamGoals = Int32.Parse(scoreMatch.Groups["gs"].Value);
										results.Add(simpleResult);
									}
									else
									{
										Debug.WriteLine("Error");
										Debug.WriteLine(row.InnerHtml);
										throw new Exception(row.InnerHtml);
									}
			             		}

			             	});

			return results;
		}

		//TODO: написать тесты
		public string GetTeamCodeById(string teamId)
		{
			lock (SyncRoot)
			{
				string teamCode = null;
				ButsaDbContext dbContext = new ButsaDbContext();
				try
				{
					var cachedTeam = dbContext.TeamsCache.FirstOrDefault(item => item.TeamId.Equals(teamId));
					if (cachedTeam != null)
					{
						if (String.IsNullOrEmpty(cachedTeam.TeamCode))
						{
							const string teamUrlTemplate = "http://butsa.ru/roster/{0}";
							const string regexTemplate =
								"<input name=\"ShortName\" type=\"hidden\" value=\"\\w+ \\({0}\\)\" /><span>(?<teamCode>\\w+) \\({0}\\)</span>";
							string pageHtml = LoadPage(String.Format(teamUrlTemplate, teamId), true);

							Regex extractCodeRegex = new Regex(String.Format(regexTemplate, teamId));
							var match = extractCodeRegex.Match(pageHtml);
							teamCode = match.Success ? match.Groups["teamCode"].Value.Trim(_trimSymbols) : null;
						}
						cachedTeam.TeamCode = teamCode;
						dbContext.SaveChanges();
					}
				}
				finally
				{
					dbContext.Dispose();
				}
				return teamCode;
			}
		}

		//TODO: написать тесты
		public string GetTeamIdByCode(string teamCode)
		{
			lock (SyncRoot)
			{
				string teamId = null;
				ButsaDbContext dbContext = new ButsaDbContext();
				try
				{
					var cachedTeam = dbContext.TeamsCache.FirstOrDefault(item => item.TeamCode.Equals(teamCode));
					if (cachedTeam == null)
					{
						const string teamUrlTemplate = "http://butsa.ru/roster/{0}";
						const string regexTemplate = "{0} \\((?<teamId>\\d+)\\)";
						string pageHtml = LoadPage(String.Format(teamUrlTemplate, teamCode), true);

						Regex extractCodeRegex = new Regex(String.Format(regexTemplate, teamCode));
						var match = extractCodeRegex.Match(pageHtml);
						teamId = match.Success ? match.Groups["teamId"].Value.Trim(_trimSymbols) : null;

						cachedTeam = dbContext.TeamsCache.FirstOrDefault(item => item.TeamId.Equals(teamId));
						if (cachedTeam != null)
						{
							cachedTeam.TeamCode = teamCode;
							dbContext.SaveChanges();
						}
					}
					else
					{
						teamId = cachedTeam.TeamId;
					}
				}
				finally
				{
					dbContext.Dispose();
				}
				return teamId;
			}
		}

		public string GetTeamIdByName(string teamName)
		{
			string result = TeamIdByNameCache.Instance.GetFromCache(teamName);
			if (!String.IsNullOrEmpty(result))
				return result;

			const string searchUrl = "http://butsa.ru/xml/ratings/users.php?step=1&type=ratings&users&firstpage=&xml&ratings&users.php%3Fact%3Dsearch&act=search&User=&TeamName={0}&UserWOTeam=1";

			string loadedPage = LoadPage(String.Format(searchUrl, teamName));
			TextReader loadedPageReader = new StringReader(loadedPage);
			HtmlDocument searchResultsDoc = new HtmlDocument();
			searchResultsDoc.Load(loadedPageReader);
			var searchResultsTable = searchResultsDoc.DocumentNode.SelectSingleNode("/body[1]/div[1]/table[1]/tr[3]/td[1]/table[1]/tr[1]/td[2]/table[1]/tr[1]/td[2]/table[1]/tr[7]/td[1]/table[1]/tr[1]/td[2]/table[1]/tr[2]/td[1]/table[1]/tr[1]/td[1]/table[1]");
			if (searchResultsTable == null)
				throw new ApplicationException("GetTeamIdByName"+teamName);

			var rowsCount = searchResultsTable.ChildNodes.Where(item => String.Compare(item.Name, "tr", true) == 0).Count();
			for (int i = 2; i <= rowsCount; i++)
			{
				var searchedTeamName = searchResultsTable.SelectSingleNode(String.Format("tr[{0}]/td[4]/a[1]", i)).InnerText;
				if (searchedTeamName.Equals(teamName))
				{
					var teamCodeHref = searchResultsTable.SelectSingleNode(String.Format("tr[{0}]/td[4]/a[1]", i)).Attributes["href"].Value;
					Regex extractCodeRegex = new Regex("/roster/(?<code>[^/]+)");
					Match match = extractCodeRegex.Match(teamCodeHref);
					if (match.Success)
					{
						var teamCode = match.Groups["code"].Captures[0].Value;
						var teamId = GetTeamIdByCode(teamCode);
						TeamIdByNameCache.Instance.AddToCache(teamName, result);
						return teamId;
					}
					throw new ApplicationException("GetTeamIdByName.match.Success");
				}
			}
			throw new ApplicationException(String.Format("not found in GetTeamIdByName: {0}", teamName));
		}

		/// <summary>
		/// Названия всех дивизионов.
		/// </summary>
		/// <returns></returns>
		public List<Division> GetAllDivisionsNames()
		{
			List<Division> divisions = new List<Division>();
			string page = LoadPage("http://butsa.ru/ratings/");
			page = page.Substring(page.IndexOf("<select name=\"Division\""));
			do
			{
				page = page.Substring(page.IndexOf("option value=\"") + 14);
				string id = page.Substring(0, page.IndexOf(">") - 2);
				string divisionName = page.Substring(page.IndexOf(">") + 1, page.IndexOf("<") - page.IndexOf(">") - 1);
				if (!String.IsNullOrEmpty(divisionName))
				{
					divisions.Add(new Division { Id=id, Name = divisionName });
				}
			}
			while (page.IndexOf("option value=") != -1);

			return divisions;
		}

		/// <summary>
		/// Возвращает команду с игроками по её идентификатору.
		/// </summary>
		/// <param name="teamId"></param>
		/// <returns></returns>
		public Team GetTeamById(string teamId)
		{
			string teamPageUrl = String.Format("http://butsa.ru/roster/{0}/", teamId);
			string teamPageHtml = LoadPage(teamPageUrl);
			TextReader teamPageReader = new StringReader(teamPageHtml);
			HtmlDocument teamPageHtmlDocument = new HtmlDocument();
			teamPageHtmlDocument.Load(teamPageReader);
			char[] trimedSymbols = new[] { ' ', '_' };

			//Чтение команды
			Team team = new Team();
			var teamTable = teamPageHtmlDocument.DocumentNode.SelectSingleNode("/body[1]/div[1]/table[1]/tr[3]/td[1]/table[1]/tr[1]/td[2]/table[1]/tr[1]/td[2]/table[1]/tr[7]/td[1]/table[1]/tr[1]/td[1]/td[1]/table[1]/tr[2]/td[1]/table[1]/tr[1]/td[1]/table[1]");
			team.TeamId = teamId;
			team.TeamName = teamTable.SelectSingleNode("td[1]/table[1]/tr[1]/td[3]/span[1]/a[1]").InnerText;
			team.DivisionName = teamTable.SelectSingleNode("td[2]/table[1]/tr[1]/td[3]/span[1]/a[1]").InnerText;
			team.CountryName = team.DivisionName.Substring(0, team.DivisionName.IndexOf('-'));
			//чек на деньги
			bool moneyCheck = teamTable.SelectSingleNode("td[2]/table[1]/tr[2]/td[1]").InnerText.Contains("Деньги");

			int k = 4;
			if (!moneyCheck)
				k = 3;
			else
				team.Money = teamTable.SelectSingleNode("td[2]/table[1]/tr[2]/td[3]").InnerText.Trim(trimedSymbols);

			string forceString = teamTable.SelectSingleNode("td[2]/table[1]/tr[" + k + "]/td[3]/span[1]").InnerText.Trim(trimedSymbols);
			forceString = forceString.Substring(0, forceString.IndexOf(' '));
			team.Force = Double.Parse(forceString, CultureInfo.InvariantCulture);
			string top11ForceString = teamTable.SelectSingleNode("td[2]/table[1]/tr[" + (k + 1) +"]/td[3]/span[1]").InnerText.Trim(trimedSymbols);
			top11ForceString = top11ForceString.Substring(0, top11ForceString.IndexOf(' '));
			team.Top11Force = Double.Parse(top11ForceString, CultureInfo.InvariantCulture);
			team.Rating = Double.Parse(teamTable.SelectSingleNode("td[2]/table[1]/tr[" + (k + 2) + "]/td[3]/span[1]").InnerText.Trim(trimedSymbols), CultureInfo.InvariantCulture);
			team.HasManager = !String.IsNullOrEmpty(teamTable.SelectSingleNode("td[1]/table[1]/tr[3]/td[3]/span[1]/nobr[1]/a[1]").InnerText.Trim(trimedSymbols));

			if (team.HasManager)
				team.Manager = teamTable.SelectSingleNode("td[1]/table[1]/tr[3]/td[3]/span[1]/nobr[1]/a[1]").InnerText.Trim(trimedSymbols);

			//Чтение игроков
			Regex idFromPlayerUrlExtractor = new Regex(@"/players/(?<id>\d*)", RegexOptions.Compiled);
			var playersTable = teamPageHtmlDocument.DocumentNode.SelectSingleNode("/body[1]/div[1]/table[1]/tr[3]/td[1]/table[1]/tr[1]/td[2]/table[1]/tr[1]/td[2]/table[1]/tr[7]/td[1]/table[1]/tr[1]/td[1]/td[1]/table[1]/tr[2]/td[1]/table[1]/tr[1]/td[1]/table[2]/tr[1]/td[1]/table[1]");
			int playersCount = playersTable.ChildNodes.Where(item => String.Compare(item.Name, "tr", true) == 0).Count();
			Parallel.For(2, playersCount, new ParallelOptions { MaxDegreeOfParallelism = 2}, i =>
			{
			    HtmlNode playerNode = playersTable.SelectSingleNode(String.Format("tr[{0}]/td[2]/a[1]", i));
			    string playerUrl = playerNode.Attributes["href"].Value;
			    Match matchResult = idFromPlayerUrlExtractor.Match(playerUrl);
			    if (matchResult.Success)
			    {
			        Player player = GetPlayerById(matchResult.Groups["id"].Captures[0].Value);
			        team.Players.Add(player);
			    }
			});

			return team;
		}

		/// <summary>
		/// Возвращает игрока по его идентификатору.
		/// </summary>
		/// <param name="playerId"></param>
		/// <returns></returns>
		public Player GetPlayerById(string playerId)
		{
			string playerPageUrl = String.Format("http://butsa.ru/players/{0}", playerId);
			string playerPageHtml = LoadPage(playerPageUrl);
			TextReader playerPageReader = new StringReader(playerPageHtml);
			HtmlDocument playerPageHtmlDocument = new HtmlDocument();
			playerPageHtmlDocument.Load(playerPageReader);

			Player player = new Player { Id = playerId };

			char[] trimedSymbols = new[] { ' ', '_' };

			//Имя и номер
			const string xPathToNameAndNumber = "/body[1]/div[1]/table[1]/tr[3]/td[1]/table[1]/tr[1]/td[2]/table[1]/tr[1]/td[2]/table[1]/tr[7]/td[1]/table[1]/tr[1]/td[1]/td[1]/table[1]/tr[2]/td[1]/table[1]/tr[1]/td[1]/table[1]/tr[1]";
			var nnRow = playerPageHtmlDocument.DocumentNode.SelectSingleNode(xPathToNameAndNumber);

			player.IsDiscvalificated = nnRow.InnerHtml.Contains("Дисквалификация");
			player.IsTraumed = nnRow.InnerHtml.Contains("Травма");

			string number = nnRow.SelectSingleNode("td[1]").FirstChild.InnerText.Trim(trimedSymbols);
			Regex parseNumberRegex = new Regex("\\((?<nmbr>.*)\\)");
			Match matchNumber = parseNumberRegex.Match(number);
			if (matchNumber.Success)
				player.Number = matchNumber.Groups["nmbr"].Captures[0].Value;
			player.Name = nnRow.SelectSingleNode("td[1]/a[1]").InnerText;

			bool isGoalkiper = nnRow.SelectSingleNode("td[1]").ChildNodes[5].InnerText.Contains("Gk");

			//Умения
			if (!isGoalkiper)
			{
				const string xPathToSkillTable = "/body[1]/div[1]/table[1]/tr[3]/td[1]/table[1]/tr[1]/td[2]/table[1]/tr[1]/td[2]/table[1]/tr[7]/td[1]/table[1]/tr[1]/td[1]/td[1]/table[1]/tr[2]/td[1]/table[1]/tr[1]/td[1]/table[1]/tr[2]/td[1]/table[1]/tr[1]/td[1]/b[1]/table[1]";
				var skillTable = playerPageHtmlDocument.DocumentNode.SelectSingleNode(xPathToSkillTable);

				player.TakingSkill = Double.Parse(skillTable.SelectSingleNode("tr[1]/td[2]/b[1]").InnerText.Trim(trimedSymbols), CultureInfo.InvariantCulture);
				player.MarkingSkill = Double.Parse(skillTable.SelectSingleNode("tr[2]/td[2]/b[1]").InnerText.Trim(trimedSymbols), CultureInfo.InvariantCulture);
				player.DribblingSkill = Double.Parse(skillTable.SelectSingleNode("tr[3]/td[2]/b[1]").InnerText.Trim(trimedSymbols), CultureInfo.InvariantCulture);
				player.ReceptionSkill = Double.Parse(skillTable.SelectSingleNode("tr[4]/td[2]/b[1]").InnerText.Trim(trimedSymbols), CultureInfo.InvariantCulture);
				player.StaminaSkill = Double.Parse(skillTable.SelectSingleNode("tr[5]/td[2]/b[1]").InnerText.Trim(trimedSymbols), CultureInfo.InvariantCulture);
				player.PassSkill = Double.Parse(skillTable.SelectSingleNode("tr[6]/td[2]/b[1]").InnerText.Trim(trimedSymbols), CultureInfo.InvariantCulture);
				player.StrikeValueSkill = Double.Parse(skillTable.SelectSingleNode("tr[7]/td[2]/b[1]").InnerText.Trim(trimedSymbols), CultureInfo.InvariantCulture);
				player.StrikeAccuracySkill = Double.Parse(skillTable.SelectSingleNode("tr[8]/td[2]/b[1]").InnerText.Trim(trimedSymbols), CultureInfo.InvariantCulture);
			}

			//Характеристики
			const string xPathToCharacteristicsTable = "/body[1]/div[1]/table[1]/tr[3]/td[1]/table[1]/tr[1]/td[2]/table[1]/tr[1]/td[2]/table[1]/tr[7]/td[1]/table[1]/tr[1]/td[1]/td[1]/table[1]/tr[2]/td[1]/table[1]/tr[1]/td[1]/table[1]/tr[2]/td[1]/table[1]/tr[1]/td[2]/b[1]/table[1]";

			var characteristicsTable = playerPageHtmlDocument.DocumentNode.SelectSingleNode(xPathToCharacteristicsTable);

			player.CurrentTotalSkill = Double.Parse(characteristicsTable.SelectSingleNode("tr[1]/td[2]/b[1]").InnerText.Trim(trimedSymbols), CultureInfo.InvariantCulture);
			player.Age = Int32.Parse(characteristicsTable.SelectSingleNode("tr[2]/td[2]/b[1]").InnerText.Trim(trimedSymbols));
			string position = characteristicsTable.SelectSingleNode("tr[3]/td[2]/b[1]").InnerText.Trim(trimedSymbols);
			Position primaryPosition;
			Position? secondaryPosition;
			CalculatePositions(position, out primaryPosition, out secondaryPosition);
			player.PrimaryPosition = primaryPosition;
			player.SecondaryPosition = secondaryPosition;

			//бонусы
			//Ск2 Ат2 Тх2 Пл2 Пр2 Пд2 Нв2 Гл2 Пн2 Шт2 Уг2 Ау2 Лд2 Ун2 Од2 Ов2
			string bonusesString = characteristicsTable.SelectSingleNode("tr[8]/td[2]/b[1]").InnerText.Trim(trimedSymbols);
			var playerBonuses = ParsePlayerBonusesString(bonusesString);
			player.BonusesList = playerBonuses
				.Select(item => new KeyValuePair<Bonus, int> {Key = item.Key, Value = item.Value})
				.ToList();

			//Текущее состояние
			const string xPathToCurrentStateTable = "/body[1]/div[1]/table[1]/tr[3]/td[1]/table[1]/tr[1]/td[2]/table[1]/tr[1]/td[2]/table[1]/tr[7]/td[1]/table[1]/tr[1]/td[1]/td[1]/table[1]/tr[2]/td[1]/table[1]/tr[1]/td[1]/table[1]/tr[2]/td[1]/table[1]/tr[2]/td[1]/b[1]/table[1]";

			var currentStateTable = playerPageHtmlDocument.DocumentNode.SelectSingleNode(xPathToCurrentStateTable);
			player.PhysicalFitness = Double.Parse(currentStateTable.SelectSingleNode("tr[1]/td[2]/b[1]").InnerText.Trim(trimedSymbols).Trim(new[] { '%' }), CultureInfo.InvariantCulture);
			string moralityString;
			if (currentStateTable.SelectSingleNode("tr[2]/td[2]/b[1]/a[1]") != null)
				moralityString = currentStateTable.SelectSingleNode("tr[2]/td[2]/b[1]/a[1]").InnerText.Trim(trimedSymbols);
			else
				moralityString = currentStateTable.SelectSingleNode("tr[2]/td[2]/b[1]").InnerText.Trim(trimedSymbols);
			Regex parseMoralityRegex = new Regex("\\((?<mrlt>.*)\\)");
			Match match = parseMoralityRegex.Match(moralityString);
			if (match.Success)
				moralityString = match.Groups["mrlt"].Captures[0].Value;
			player.Morality = Int32.Parse(moralityString);

			player.Tiredness = Double.Parse(currentStateTable.SelectSingleNode("tr[3]/td[2]/b[1]").InnerText.Trim(trimedSymbols), CultureInfo.InvariantCulture);
			//player.ActualTotalSkill = Double.Parse(currentStateTable.SelectSingleNode("tr[4]/td[2]/b[1]").InnerText.Trim(trimedSymbols), CultureInfo.InvariantCulture);

			return player;
		}

		private Dictionary<Bonus, int> ParsePlayerBonusesString(string bonusesString)
		{
			Dictionary<Bonus, int> playerBonuses = new Dictionary<Bonus, int>();
			Regex parseBonusRegex = new Regex(@"(?<bns>\w\w\d?)");
			if (!String.IsNullOrEmpty(bonusesString) && bonusesString.Trim() != "-")
			{
				Match bonusMatch = parseBonusRegex.Match(bonusesString);
				if (bonusMatch.Success)
				{
					while (bonusMatch.Success)
					{
						string bns = bonusMatch.Groups["bns"].Captures[0].Value;
						string bnsName = bns.Substring(0, 2);
						int bnsCount = 1;
						if (bns.Length > 2)
							bnsCount = Int32.Parse(bns.Substring(2, 1));
						playerBonuses[BonusNameToEnum(bnsName)] = bnsCount;
						bonusMatch = bonusMatch.NextMatch();
					}
				}
				else
				{
					throw new ApplicationException("Ошибка разбора бонуса.");
				}
			}
			return playerBonuses;
		}

		public List<ForecastPair> ReadForecastTable(EventHandler<ProgressEventArgs> progressCallback = null)
		{
			const string foreCastTableUrl = "http://butsa.ru/xml/tour/kp.php?act=next";
			string foreCastTableHtml = LoadPage(foreCastTableUrl);
			TextReader foreCastTableReader = new StringReader(foreCastTableHtml);
			HtmlDocument foreCastTableHtmlDocument = new HtmlDocument();
			foreCastTableHtmlDocument.Load(foreCastTableReader);

			char[] trimedSymbols = new[] { ' ', '_', '\n' };

			const string tableXPath = "/body[1]/div[1]/table[1]/tr[3]/td[1]/table[1]/tr[1]/td[2]/table[1]/tr[1]/td[2]/table[1]/tr[7]/td[1]/table[1]/tr[1]/td[1]/td[1]/table[1]/tr[2]/td[1]/table[1]/tr[1]/td[1]/div[3]/table[1]";
			var foreCastTable = foreCastTableHtmlDocument.DocumentNode.SelectSingleNode(tableXPath);
			var rows = foreCastTable.ChildNodes
				.Where(item => String.Compare(item.Name, "tr", true) == 0)
				.Select(item => item)
				.ToList();

			if (progressCallback != null)
				progressCallback.Invoke(this, new ProgressEventArgs(1.0));

			//записи начинаются с tr2
			int processed = 0;
			List<ForecastPair> foreCastPairs = new List<ForecastPair>();
			Parallel.For(1, rows.Count, new ParallelOptions { MaxDegreeOfParallelism = 2}, i => 
			{
				try
				{
					var rowNode = rows[i];

					string homeTeamName = rowNode.SelectSingleNode("td[2]").InnerText.Trim(trimedSymbols);
					string guestTeamName = rowNode.SelectSingleNode("td[6]/div[1]").InnerText.Trim(trimedSymbols);

					string homeTeamId = GetTeamIdByName(homeTeamName);
					string guestTeamId = GetTeamIdByName(guestTeamName);

					Team homeTeam = GetTeamById(homeTeamId);
					Team guestTeam = GetTeamById(guestTeamId);

					ForecastPair foreCastPair = new ForecastPair {HomeTeam = homeTeam, GuestTeam = guestTeam};
					foreCastPairs.Add(foreCastPair);

					if (progressCallback != null)
						progressCallback.Invoke(this,
						                        new ProgressEventArgs(1.0 + (double) ++processed/rows.Count*99,
						                                              String.Format("Загружена пара {0} - {1}", homeTeam.TeamName,
						                                                            guestTeam.TeamName)));
				} catch(Exception ex)
				{
				    Debug.WriteLine(ex.ToString());
					throw;
				}
			
			});
			if (progressCallback != null)
				progressCallback.Invoke(this, new ProgressEventArgs(100.0));
			return foreCastPairs;
		}

		public void ReadForecastTableLight(EventHandler<ProgressEventArgs> progressCallback, 
			EventHandler<ReadForecastCompletedArgs> completeCallback)
		{
			//const string foreCastTableUrl = "http://butsa.ru/xml/tour/kp.php?act=next";
			const string foreCastTableUrl = "http://butsa.ru/kp/?act=next";
			string foreCastTableHtml = LoadPage(foreCastTableUrl);
			TextReader foreCastTableReader = new StringReader(foreCastTableHtml);
			HtmlDocument foreCastTableHtmlDocument = new HtmlDocument();
			foreCastTableHtmlDocument.Load(foreCastTableReader);

			char[] trimedSymbols = new[] { ' ', '_', '\n' };

			const string tableXPath = "/body[1]/div[1]/table[1]/tr[3]/td[1]/table[1]/tr[1]/td[2]/table[1]/tr[1]/td[2]/table[1]/tr[7]/td[1]/table[1]/tr[1]/td[1]/td[1]/table[1]/tr[2]/td[1]/table[1]/tr[1]/td[1]/div[3]/table[1]";
			var foreCastTable = foreCastTableHtmlDocument.DocumentNode.SelectSingleNode(tableXPath);
			var rows = foreCastTable.ChildNodes
				.Where(item => String.Compare(item.Name, "tr", true) == 0)
				.Select(item => item)
				.ToList();

			if (progressCallback != null)
				progressCallback.Invoke(this, new ProgressEventArgs(1.0));

			//записи начинаются с tr2
			int processed = 0;
			List<ForecastPairRef> foreCastPairs = new List<ForecastPairRef>();
			Parallel.For(1, rows.Count, new ParallelOptions { MaxDegreeOfParallelism = 2 }, i =>
			{
				try
				{
					var rowNode = rows[i];

					string homeTeamName = rowNode.SelectSingleNode("td[2]").InnerText.Trim(trimedSymbols);
					string guestTeamName = rowNode.SelectSingleNode("td[5]/div[1]").InnerText.Trim(trimedSymbols);

					string homeTeamId = GetTeamIdByName(homeTeamName);
					string guestTeamId = GetTeamIdByName(guestTeamName);

					ForecastPairRef foreCastPair = new ForecastPairRef 
					{ 
						HomeTeamId = Int32.Parse(homeTeamId), 
						GuestTeamId = Int32.Parse(guestTeamId) 
					};
					foreCastPairs.Add(foreCastPair);

					if (progressCallback != null)
						progressCallback.Invoke(this,
												new ProgressEventArgs(1.0 + (double)++processed / rows.Count * 99,
																	  String.Format("Прочитана пара {0} - {1}", homeTeamName,
																					guestTeamName)));
				}
				catch (Exception ex)
				{
					Debug.WriteLine(ex.ToString());
					throw;
				}

			});
			if (progressCallback != null)
				progressCallback.Invoke(this, new ProgressEventArgs(100.0, "done", true));
			if (completeCallback != null)
				completeCallback.Invoke(this, new ReadForecastCompletedArgs { ForecastPairs = foreCastPairs });
		}

		public List<ForecastPairRef> ReadForecastTableLight()
		{
			//const string foreCastTableUrl = "http://butsa.ru/xml/tour/kp.php?act=next";
			const string foreCastTableUrl = "http://butsa.ru/kp/?act=next";
			string foreCastTableHtml = LoadPage(foreCastTableUrl);
			TextReader foreCastTableReader = new StringReader(foreCastTableHtml);
			HtmlDocument foreCastTableHtmlDocument = new HtmlDocument();
			foreCastTableHtmlDocument.Load(foreCastTableReader);

			char[] trimedSymbols = new[] { ' ', '_', '\n' };

			const string tableXPath = "/body[1]/div[1]/table[1]/tr[3]/td[1]/table[1]/tr[1]/td[2]/table[1]/tr[1]/td[2]/table[1]/tr[7]/td[1]/table[1]/tr[1]/td[1]/td[1]/table[1]/tr[2]/td[1]/table[1]/tr[1]/td[1]/div[3]/table[1]";
			var foreCastTable = foreCastTableHtmlDocument.DocumentNode.SelectSingleNode(tableXPath);
			var rows = foreCastTable.ChildNodes
				.Where(item => String.Compare(item.Name, "tr", true) == 0)
				.Select(item => item)
				.ToList();

			//записи начинаются с tr2
			int processed = 0;
			List<ForecastPairRef> foreCastPairs = new List<ForecastPairRef>();
			Parallel.For(1, rows.Count, new ParallelOptions { MaxDegreeOfParallelism = 2 }, i =>
			{
				try
				{
					var rowNode = rows[i];

					string homeTeamName = rowNode.SelectSingleNode("td[2]").InnerText.Trim(trimedSymbols);
					string guestTeamName = rowNode.SelectSingleNode("td[5]/div[1]").InnerText.Trim(trimedSymbols);

					string homeTeamId = GetTeamIdByName(homeTeamName);
					string guestTeamId = GetTeamIdByName(guestTeamName);

					ForecastPairRef foreCastPair = new ForecastPairRef
					{
						HomeTeamId = Int32.Parse(homeTeamId),
						GuestTeamId = Int32.Parse(guestTeamId)
					};
					foreCastPairs.Add(foreCastPair);
				}
				catch (Exception ex)
				{
					Debug.WriteLine(ex.ToString());
					throw;
				}

			});
			return foreCastPairs;
		}

		private void CalculatePositions(string position, out Position primaryPosition, out Position? secondaryPosition)
		{
			string primaryPositionString, secondaryPositionString = String.Empty;
			
			if (position.Length <= 3)
				primaryPositionString = position;
			else
			{
				int slashPosition = position.IndexOf('/');
				primaryPositionString = position.Substring(0, slashPosition);
				secondaryPositionString = position.Substring(slashPosition + 1, position.Length - slashPosition - 1);
			}

			primaryPosition = (Position) Enum.Parse(typeof(Position), primaryPositionString, true);
			if (!String.IsNullOrEmpty(secondaryPositionString))
				secondaryPosition = (Position)Enum.Parse(typeof(Position), secondaryPositionString, true);
			else
				secondaryPosition = null;
		}

		public Score GetMatchResultForTeams(string homeTeamId, string guestTeamId)
		{
			const string lastResultsPageUrlTemplate = "http://butsa.ru/xml/players/roster.php?act=allmatches&id={0}";
			string lastResultsPageContent = LoadPage(String.Format(lastResultsPageUrlTemplate, homeTeamId));
			TextReader lastResultsPageContentReader = new StringReader(lastResultsPageContent);
			HtmlDocument lastResultsPageDocument = new HtmlDocument();
			lastResultsPageDocument.Load(lastResultsPageContentReader);

			char[] trimedSymbols = new[] { ' ', '_', '\n' };
			Regex extractCodeRegex = new Regex("/roster/(?<code>[^/]+)/");
			Regex extractScore = new Regex(@"(?<hs>\d+):(?<gs>\d+)");

			const string tableXPath = "/body[1]/div[1]/table[1]/tr[3]/td[1]/table[1]/tr[1]/td[2]/table[1]/tr[1]/td[2]/table[1]/tr[7]/td[1]/table[1]/tr[1]/td[1]/td[1]/table[1]/tr[2]/td[1]/table[1]/tr[1]/td[1]/table[2]/tr[1]/td[1]/table[1]";

			var lastResultsTable = lastResultsPageDocument.DocumentNode.SelectSingleNode(tableXPath);
			var rows = lastResultsTable.ChildNodes
				.Where(item => String.Compare(item.Name, "tr", true) == 0)
				.Select(item => item)
				.ToList();

			for (int i = 1; i < rows.Count; i++)
			{
				var tr = rows[i];

				bool championatMark = tr.SelectSingleNode("td[5]").InnerText.Contains("Чемпионат страны");
				if (!championatMark)
					continue;

				var readedHomeTeamCodeAttribute = tr.SelectSingleNode("td[2]/a[1]").Attributes["href"].Value;
				var hMatch = extractCodeRegex.Match(readedHomeTeamCodeAttribute);
				if (!hMatch.Success)
					return null;
				string readedHomeTeamCode = hMatch.Groups["code"].Captures[0].Value;

				var readedGuestTeamCodeAttribute = tr.SelectSingleNode("td[4]/div[1]/a[1]").Attributes["href"].Value;
				var gMatch = extractCodeRegex.Match(readedGuestTeamCodeAttribute);
				if (!gMatch.Success)
					return null;
				string readedGuestTeamCode = gMatch.Groups["code"].Captures[0].Value;

				string readedHomeTeamId = GetTeamIdByCode(readedHomeTeamCode);
				string readedGuestTeamId = GetTeamIdByCode(readedGuestTeamCode);

				if (String.Compare(homeTeamId, readedHomeTeamId, true) == 0 && String.Compare(guestTeamId, readedGuestTeamId, true) == 0)
				{
					var scoreNode = tr.SelectSingleNode("td[3]/center[1]/a[1]/b[1]").InnerText.Trim(trimedSymbols);
					Match m = extractScore.Match(scoreNode);
					if (m.Success)
					{
						Score score = new Score
						{
							HomeTeamGoals = Int32.Parse(m.Groups["hs"].Captures[0].Value),
							GuestTeamGoals = Int32.Parse(m.Groups["gs"].Captures[0].Value)
						};
						return score;
					}
					else
						throw new ApplicationException("Не удалось распознать счёт.");
				}
			}
			return null;
		}

		private Bonus BonusNameToEnum(string bns)
		{
			//Ск2 Ат2 Тх2 Пл2 Пр2 Пд2 Нв2 Гл2 Пн2 Шт2 Уг2 Ау2 Лд2 Ун2 Од2 Ов2
			switch (bns)
			{
				case "Ск":
					return Bonus.Speed;
				case "Ат":
					return Bonus.Athleticism;
				case "Тх":
					return Bonus.Technique;
				case "Пл":
					return Bonus.Playmaker;
				case "Пр":
					return Bonus.Interception;
				case "Пд":
					return Bonus.Roll;
				case "Нв":
					return Bonus.Lob;
				case "Гл":
					return Bonus.HeadPlay;
				case "Пн":
					return Bonus.Penalti;
				case "Шт":
					return Bonus.FreeKick;
				case "Уг":
					return Bonus.Corner;
				case "Ау":
					return Bonus.Out;
				case "Лд":
					return Bonus.Lider;
				case "Ун":
					return Bonus.Universal;
				case "Од":
					return Bonus.OneToOne;
				case "Ов":
					return Bonus.GateKick;
			}
			throw new ArgumentException("bns");
		}

		public void ReadTeams(List<string> teamIds, EventHandler<ProgressEventArgs> progressCallback,
			EventHandler<ReadTeamsCompletedArgs> completeCallback)
		{
			if (progressCallback != null)
				progressCallback(this, new ProgressEventArgs(0, String.Empty));
			List<Team> resultList = new List<Team>(48);
			for (int i = 0; i < teamIds.Count; i++)
			{
				string teamId = teamIds[i];
				Team team = GetTeamById(teamId);
				resultList.Add(team);
				if (progressCallback != null)
					progressCallback(this, new ProgressEventArgs(((double)i/teamIds.Count)*100, team.TeamName));
			}
			if (progressCallback != null)
				progressCallback(this, new ProgressEventArgs(100, "done", true));
			if (completeCallback != null)
				completeCallback.Invoke(this, new ReadTeamsCompletedArgs {  Teams = resultList});
		}
	}
}
